console.assert y la comparacion NaN===NaN

Otro artículo con la opinión de José Antonio Rojas Delgado, 26/3/2024, © rojasdelgado.com

Introducción a TDD

TDD, siglas inglesas de Test Driven Development, es un paradigma de programación que consiste en escribir código para pruebas unitarias de software antes incluso de escribir el código para el propio software de negocio.

De este modo el programa está emitiendo errores de diseño incluso antes de haber implementado ni remotamente el código correcto.

Es una filosofía que ya he criticado en otros artículos, pero que ahora voy entendiendo mejor a medida que la introduzco en mis desarrollos.

En particular ahora estoy escribiendo código de pruebas unitarias para código javascript/typescript, ayudado por JTest, Vitest, Wallaby, Quokka, ... y, en un momento dado, me he planteado que, en realidad, el propio lenguaje javascript ya incorpora funcionalidades parecidas a it, expect, describe, etc...: console.assert()

console.assert(
Condición,
mensajeSiNoSeCumpleLaCondición
);

console.assert recibe dos parámetros:

Si la condición se cumple, la ejecución de console.assert(condición...) no produce ninguna salida. Si la condición no se cumple, se obtendrá un mensaje de error por consola, en letras llamativas (usualmente rojas), que mostrarán una indicación de aserción y el mensaje especificado en el segundo parámetro.

Parecería que este comportamiento de console.assert bastaría para tener un control rudimentario de la calidad (validez) del código fuente, ya que, codificadas las distintas aserciones en un lugar determinado, arbitrariamente al inicio de la ejecución del código, alertaría en consola sobre la No Funcionalidad del código javascript antes de su despliegue.

Al menos así me lo ha parecido durante mucho tiempo...

...hasta ahora.

La función

A modo de ejemplo, me planteé escribir una función para restar dos números naturales.

En el conjunto de los Números Naturales está definida la resta siempre que su resultado pertenezca a dicho conjunto. Más allá de la discusión de si se acepta el 0 en este conjunto, lo cierto es que si al restar se obtiene un número negativo, este resultado no es válido, y debería considerarse, o bien un resultado indefinido o en todo caso un resultado que no es un número natural.

De este modo la función quedaría


function restanatural(a,b)
{
    var r=undefined;
    if(a>b){
        r=a-b;
    }
    else
    {
        r=NaN;
    }
    return r;
}
        

El test vía console.assert()

Se puede entender fácilmente el siguiente código fuente:


console.clear();
var param1, param2, resultadoatestear, resultadoesperado;
param1=3;
param2=2;
resultadoatestear=restanatural(param1,param2);
resultadoesperado=1;
console.assert(resultadoatestear===resultadoesperado,`La resta de ${param1} y ${param2} en los números naturales debe ser ${resultadoesperado}, y da ${resultadoatestear}`);
//
param2=7;
resultadoatestear=restanatural(param1,param2);
resultadoesperado=NaN;
console.assert(resultadoatestear===resultadoesperado,`La resta de ${param1} y ${param2} en los números naturales debe ser ${resultadoesperado}, y da ${resultadoatestear}`);
        

Se observa que se gestionan dos casos:

El resultado del test vía console.assert()

La salida del test console.assert() SE PRODUCE SIEMPRE.

El mensaje de error de aserción siempre se muestra:
Error assert: La resta de 3 y 7 en los números naturales debe ser NaN, y da NaN

¿qué?

Pero si aparentemente todo está bien escrito, ¿por qué se muestra siempre el error de aserción?

La explicación

El problema está causado por el comportamiento extraño del valor NaN.

En efecto, la comparación estricta (con los tres iguales ====) de NaN con NaN, devuelve false. Esto es


        console.log(NaN===NaN); //false
    

Es uno de los comportamientos más raros del planeta programable en el que vivimos.

Aparentemente.

Permítanme darles una explicación (que no justificación):

Resulta que el lenguaje javascript obedece a una norma, la IEEE 754, que determina, por ej,
que la operación matemática 0/0 devuelva NaN,
y que la operación matemática CualquierNumeroDistintoDeCero/0 devuelva Infinity.

Por tanto
0/0===0/0
da
((0/0)===(0/0))
que es
NaN===NaN
o sea
false

Increíble pero cierto.

Esto hace que la condición del assert, finalmente evaluada como NaN===NaN, siempre se evalúe a false, que siempre se dispare y que siempre parezca que el código está mal escrito, a pesar de la apariencia de no estarlo.

La solución que quiere IEEE 754

Existe una solución para arreglar el código y que todo funciones:

cambiar NaN por undefined

¿Pero ésto no es rizar el rizo?

¿Acaso no se entiende mejor el código que devuelve NaN cuando el resultado "NO ES UN NÚMERO NATURAL"?

Quizá sí, pero no es lo que quiere IEEE 754


function restanatural(a,b)
{
    var r=undefined;
    if(a>b){
        //r=33; //forzar fallo en verificación
        r=a-b;
    }
    else
    {
        //r=4; //forzar fallo en verificación
        r=undefined;
    }
    return r;
}

console.clear();
var param1, param2, resultadoatestear, resultadoesperado;
param1=3;
param2=2;
resultadoatestear=restanatural(param1,param2);
resultadoesperado=1;
console.assert(resultadoatestear===resultadoesperado,`La resta de ${param1} y ${param2} en los números naturales debe ser ${resultadoesperado}, y da ${resultadoatestear}`);
//
param2=7;
resultadoatestear=restanatural(param1,param2);
resultadoesperado=undefined;
console.assert(resultadoatestear===resultadoesperado,`La resta de ${param1} y ${param2} en los números naturales debe ser ${resultadoesperado}, y da ${resultadoatestear}`);
console.log("Al comparar undefined con undefined da " + (undefined===undefined));
//
console.log("Resta natural de 13 y 9 da "+restanatural(13,9));
console.log("Resta natural de 9 y 13 da "+restanatural(9,13));

module.exports=restanatural;
        

Mi solución

Que no pueda evitar que NaN===NaN sea falso no significa que no pueda escribir mi propia función de comparación para usar donde quiera, incluyendo la definición de la condición de console.assert().

Así que ahí va:


function assertEsIgual(a, b) {
    if (isNaN(a) && isNaN(b)) {
        return true;
    }
    return a === b;
}
    

... que se usa así:


console.assert(assertEsIgual(resultadoatestear,resultadoesperado),`La resta de ${param1} y ${param2} en los números naturales debe ser ${resultadoesperado}, y da ${resultadoatestear}`);
    

... y que evita definitivamente la molesta activación del mensaje assert tan inesperado como absurdo.

Obsérvese que cuando solo uno de los parámetros, a o b, es NaN, el resultado retornado es, o NaN===b o a===NaN, que, afortunadamente, devuelven false, ya que NaN es estrictamente distinto de cualquier otra cosa.

Faltaría más, ya que es estrictamente distinto de sí mismo, solo faltaría que fuese estrictamente igual a algo. Sería para suicidarse (al menos hasta poder darse una vuelta por la cafetería).








EL CÓDIGO FINAL


function restanatural(a,b)
{
    var r=undefined;
    if(a>b){
        //r=33; //forzar fallo en verificación
        r=a-b;
    }
    else
    {
        //r=4; //forzar fallo en verificación
        r=NaN;
    }
    return r;
}

console.clear();
var param1, param2, resultadoatestear, resultadoesperado;
param1=3;
param2=2;
resultadoatestear=restanatural(param1,param2);
resultadoesperado=1;
console.assert(resultadoatestear===resultadoesperado,`La resta de ${param1} y ${param2} en los números naturales debe ser ${resultadoesperado}, y da ${resultadoatestear}`);
//
param2=7;
resultadoatestear=restanatural(param1,param2);
resultadoesperado=NaN;
//console.assert(resultadoatestear===resultadoesperado,`La resta de ${param1} y ${param2} en los números naturales debe ser ${resultadoesperado}, y da ${resultadoatestear}`);
//console.log("Al comparar NaN con NaN da " + (NaN===NaN) + " por gentileza de IEEE 754 que espera 0/0="+(0/0)+" aunque CualquierNumeroDistintoDeCero/0==="+(Math.max(Math.random(),Number.MIN_VALUE)/0)+", y por tanto 0/0===0/0 da " + ((0/0)===(0/0)));
//
console.log("Resta natural de 13 y 9 da "+restanatural(13,9));
console.log("Resta natural de 9 y 13 da "+restanatural(9,13));

function assertEsIgual(a, b) {
    if (isNaN(a) && isNaN(b)) {
        return true;
    }
    return a === b;
}
console.assert(assertEsIgual(resultadoatestear,resultadoesperado),`La resta de ${param1} y ${param2} en los números naturales debe ser ${resultadoesperado}, y da ${resultadoatestear}`);


module.exports=restanatural;